﻿/*
ParallelPlatform Project - A web platform to distribute and share knowledge
Online Editor - An online tool to create and play animations
Copyright (C) 2011 Daniel Guerrero Martínez & Sergio Rodríguez Lumley

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package Classes{
	import flash.display.MovieClip;
	import flash.display.DisplayObject;
	
	import flash.text.TextField;
	import flash.text.TextFormat;
	import flash.text.Font;
	import flash.text.AntiAliasType;
	
	import flash.utils.Timer;
	import flash.utils.getDefinitionByName;
	
	import flash.events.MouseEvent;
	import flash.events.TimerEvent;
	
	import fl.transitions.Tween;
	import fl.transitions.easing.*;
	import fl.transitions.TweenEvent;
	
	import flash.filters.BitmapFilterQuality;
	import flash.filters.BlurFilter;
	
	public class PlayerAnimation extends MovieClip {
		
		private const NORMAL_DELAY = 2; // Number of seconds a Tween should last
		
		private const SCENE_INSTANT:uint = 0;
		private const SCENE_NORMAL:uint = 1;
		private const SCENE_READER:uint = 2;
		
		private var title:String; // title of the movie
		private var description:String;
		private var delay:Number; // delay of each normal scene
		
		private var sceneList:XMLList;
		private var loadedScene:uint;
		private var numScenes:uint;
		private var controllerHasEvent:Boolean;
		private var nextSceneTimer:Timer;
		private var tweenArray:Array; // This array prevents tweens from being removed by Garbage Collector
		
		private var tf_title:TextField;
		private var tf_description:TextField;
		private var controller:PlayerController;
		private var shapes:ShapeHashTable;
		private var sceneType:uint;
		private var canvas:Panel;
		private var replayArrow:PlayAgain;
		
		public function PlayerAnimation(){
			
			var myFont:Font = new Font_SegoePrint();
			var titleFormat:TextFormat 	= new TextFormat();
			var descFormat:TextFormat 	= new TextFormat();
			titleFormat.font			= myFont.fontName;
			titleFormat.align 			= "center";
			titleFormat.size			= 36;
			descFormat.font				= myFont.fontName;
			descFormat.size				= 16;
			
			canvas						= new Panel();
			canvas.mouseChildren		= false;
			
			tf_title 					= new TextField();
			tf_description 				= new TextField();
			tf_title.x 					= 50;
			tf_title.y 					= 30;
			tf_title.width 				= 600;
			tf_title.multiline			= true;
			tf_title.wordWrap			= true;
			tf_title.selectable			= false;
			tf_title.embedFonts			= true;
			tf_title.antiAliasType 		= AntiAliasType.ADVANCED;
			
			tf_description.x 			= 50;
			tf_description.y 			= 100;
			tf_description.width 		= 600;
			tf_description.height		= 350;
			tf_description.multiline	= true;
			tf_description.wordWrap		= true;
			tf_description.selectable	= false;
			tf_description.embedFonts	= true;
			tf_description.antiAliasType = AntiAliasType.ADVANCED;
			
			tf_title.defaultTextFormat 	= titleFormat;
			tf_description.defaultTextFormat = descFormat;
						
			controller 					= new PlayerController(continuePlaying);
			controller.x 				= 350;
			controller.y 				= 465;
			controllerHasEvent			= false;
			
			replayArrow					= new PlayAgain();
			replayArrow.x				= 350;
			replayArrow.y				= 400;
			
			sceneType					= SCENE_NORMAL;
			mouseChildren				= true;
			
			this.addChild(canvas);
		}
		
		/**
		  Starts the animation while starts loading all the content
		  of the scenes
		  @param xmlFile XML containing all information about the animation
		*/
		public function setXMLfile(xmlFile:XML):void{
			
			title 		= BasicFileModule.getTitle(xmlFile);//.header.title.text();
			description = BasicFileModule.getDescription(xmlFile);//
			delay		= BasicFileModule.getDelay(xmlFile);//.header.delay;
			
			sceneList	= BasicFileModule.getSceneList(xmlFile);
			loadedScene = 0;
			numScenes	= sceneList.length();
			shapes		= new ShapeHashTable();
			
			tf_title.text = title;
			//if(description.length > 0)
			tf_description.text = description;
			//else
			//	tf_description.text = "No description added";
			
		}
		
		public function begin(param = null):void{
			
			sceneType 		= SCENE_READER;
			loadedScene 	= 0;
			tweenArray		= new Array();
			clearCanvas();
			
			nextSceneTimer 	= new Timer(NORMAL_DELAY*1000, 1);
			nextSceneTimer.stop();
			nextSceneTimer.addEventListener(TimerEvent.TIMER_COMPLETE, continuePlaying);
			canvas.removeEventListener(MouseEvent.CLICK, begin);
			
			tf_title.alpha 			= 0;
			tf_description.alpha 	= 0;
			controller.alpha 		= 0;
			if(tf_title.parent == null) canvas.addChild(tf_title);
			if(tf_description.parent == null) canvas.addChild(tf_description);
			
			appear(tf_title);
			appear(tf_description, true);
		}
		
		/**
		  Stops the animation playing and removes all event listeners, leaves
		  the player as in initial state (to continue after this, animation
		  must start from the begining, using begin() function)
		*/
		public function halt():void{
			controller.restart();
			controller.stopCount();
			if(controllerHasEvent){
				controller.removeEventListener(MouseEvent.CLICK, controllerOnClick);
				canvas.removeEventListener(MouseEvent.CLICK, continuePlaying);
				controllerHasEvent = false;
			}
			if(replayArrow.parent != null){
				this.removeChild(replayArrow);
				replayArrow.end();
			}
			
			for each (var tween:Tween in tweenArray){
				tween.stop();
				tween.removeEventListener(TweenEvent.MOTION_FINISH, lastTweenFinished);
			}
			replayArrow.removeEventListener(MouseEvent.CLICK, restartMovie);
			canvas.removeEventListener(MouseEvent.CLICK, restartMovie);
			nextSceneTimer.stop();
			nextSceneTimer.removeEventListener(TimerEvent.TIMER_COMPLETE, continuePlaying);
		}
		
		/********* PRIVATE FUNCTIONS ***********/
		
		/**
		  Starts loading the next scene, or shows the end scene if there are no more
		*/
		private function loadNextScene():void{
			++loadedScene;
			if(loadedScene > numScenes){
				// We have finished!
				if(tf_title.parent == null) canvas.addChild(tf_title);
				if(tf_description.parent == null) canvas.addChild(tf_description);
				canvas.addChildAt(new WhitePanel(), canvas.getChildIndex(tf_title) );
				appear(canvas.getChildAt(canvas.getChildIndex(tf_title)-1));
				appear(tf_title);
				appear(tf_description);
				this.addChild(replayArrow);
				replayArrow.startPlay();
				replayArrow.addEventListener(MouseEvent.CLICK, restartMovie);
				canvas.addEventListener(MouseEvent.CLICK, restartMovie);
			}else{
				loadScene(loadedScene-1);
			}
		}
		
		/**
		  Restarts the movie after WhitePanel has dissapeared
		  @param param not used parameter, null by default
		*/
		private function restartMovie(param = null):void{
			replayArrow.removeEventListener(MouseEvent.CLICK, restartMovie);
			canvas.removeEventListener(MouseEvent.CLICK, restartMovie);
			replayArrow.finish();
			disappearNonShapeObject(tf_title);
			disappearNonShapeObject(tf_description);
			disappearNonShapeObject(canvas.getChildAt(canvas.getChildIndex(tf_title)-1), begin, true);
		}
		
		/**
		  Completely loads the scene into the animation
		  @param num number of the scene
		*/
		private function loadScene(num:uint):void{
			var actions:XMLList;
			var i:uint = 0;
			var islast:Boolean = false;
			
			sceneType = getSceneTypeCode(BasicFileModule.getSceneType(sceneList[num]));
			actions = BasicFileModule.getSceneActions(sceneList[num]);
			
			if(actions.length() <= 0){
				lastTweenFinished();
			}else{
				for each(var action:XML in actions){
					if(++i >= actions.length()) islast = true;
					parseAction(action, islast);
				}
			}
		}
		
		/**
		  Starts loading the next scene
		*/
		private function continuePlaying(arg = null):void{
			nextSceneTimer.stop();
			if(loadedScene == 0){ // The first continue must make dissapear everything manually
				disappearNonShapeObject(tf_title);
				disappearNonShapeObject(tf_description);
			}
			if(sceneType == SCENE_READER){
				controller.stopCount();
				disappearNonShapeObject(controller);
				canvas.removeEventListener(MouseEvent.CLICK, continuePlaying);
				controller.removeEventListener(MouseEvent.CLICK, controllerOnClick);
				controllerHasEvent = false;
			}
			loadNextScene();
		}
		
		/**
		  Last tween has finished animating
		  @param evt TweenEvent by default null (it is not used)
		*/
		private function lastTweenFinished(evt:TweenEvent=null):void{
			
			// Now that all of the Tweens have finished, we empty
			// the tween array to keep load in memory to the minimum
			tweenArray = new Array();
			
			switch(sceneType){
				case SCENE_INSTANT:
					continuePlaying();
					break;
				case SCENE_NORMAL:
					nextSceneTimer.start();
					break;
				case SCENE_READER:
					canvas.addEventListener(MouseEvent.CLICK, continuePlaying);
					controller.addEventListener(MouseEvent.CLICK, controllerOnClick);
					controllerHasEvent = true;
					this.addChild(controller);
					appear(controller);
					controller.begin();
					break;
			}
		}
		
		private function controllerOnClick(evt:MouseEvent):void{
			controller.swapStatus();
		}
		
		/**
		  Makes the object appear
		*/
		private function appear(obj, isLastTween:Boolean = false):void{
			var tween:Tween = new Tween(obj, "alpha", None.easeInOut, 0, 1, delay, true);
			if(isLastTween){
				tween.addEventListener(TweenEvent.MOTION_FINISH, lastTweenFinished);
			}
			tweenArray.push(tween);
		}
		
		/**
		  Makes the object disappear
		*/
		private function disappear(obj, isLastTween:Boolean = false):void{
			var tween:Tween = new Tween(obj, "alpha", None.easeInOut, 1, 0, delay, true);
			tween.addEventListener(TweenEvent.MOTION_FINISH, removeObj);
			if(isLastTween){
				tween.addEventListener(TweenEvent.MOTION_FINISH, lastTweenFinished);
			}
			tweenArray.push(tween);
		}
		
		private function moveTo(obj:PlayerShapeObject, posx:Number, posy:Number, isLastTween:Boolean = false):void{
			var tween1:Tween = new Tween(obj, "x", Regular.easeInOut, obj.x, posx, delay, true)
			var tween2:Tween = new Tween(obj, "y", Regular.easeInOut, obj.y, posy, delay, true);
			if(isLastTween){
				if(Math.abs(posx - obj.x) > Math.abs(posy - obj.y))
					tween1.addEventListener(TweenEvent.MOTION_FINISH, lastTweenFinished);
				else
					tween2.addEventListener(TweenEvent.MOTION_FINISH, lastTweenFinished);
			}
			tweenArray.push(tween1);
			tweenArray.push(tween2);
		}
		
		private function changeText(obj:PlayerShapeObject, newText:String, isLastTween:Boolean = false):void{
			var tween:Tween = new Tween(obj, "textChange", None.easeOut, 0, 100, delay, true);
			obj.setText(newText);
			if(isLastTween){
				tween.addEventListener(TweenEvent.MOTION_FINISH, lastTweenFinished);
			}
			tweenArray.push(tween);
		}
		
		private function changeDepth(obj:PlayerShapeObject, newDepth:uint, isLastTween:Boolean = false):void{
			var tween:Tween = new Tween(obj, "depthChange", None.easeOut, 0, 100, delay, true);
			//new Tween(obj, "scaleX", Elastic.easeOut, obj.scaleX/2, obj.scaleX, delay, true);
			//tweenArray.push(new Tween(obj, "scaleY", Elastic.easeOut, obj.scaleY/2, obj.scaleY, delay, true));
			setShapeDepth(obj, newDepth);
			if(isLastTween){
				tween.addEventListener(TweenEvent.MOTION_FINISH, lastTweenFinished);
			}
			tweenArray.push(tween);
		}
		
		private function changeScale(obj, newScaleX:Number, newScaleY:Number, isLastTween:Boolean = false):void{
			var tween:Tween;
			
			if(obj.scaleX != newScaleX){
				tween = new Tween(obj, "scaleX", Regular.easeInOut, obj.scaleX, newScaleX, delay, true);
				tweenArray.push(tween);
			}
			if(obj.scaleY != newScaleY){
				tween = new Tween(obj, "scaleY", Regular.easeInOut, obj.scaleY, newScaleY, delay, true);
				tweenArray.push(tween);
			}

			if(isLastTween){
				tween.addEventListener(TweenEvent.MOTION_FINISH, lastTweenFinished);
			}
		}
		
		private function changeRotation(obj, newRot:Number, isLastTween:Boolean = false):void{
			var tween:Tween = new Tween(obj, "rotation", Regular.easeInOut, obj.rotation, newRot, delay, true);
			tweenArray.push(tween);
			if(isLastTween){
				tween.addEventListener(TweenEvent.MOTION_FINISH, lastTweenFinished);
			}
		}
		
		private function changeBlur(obj:PlayerShapeObject, newBlurX:Number, newBlurY:Number, isLastTween:Boolean = false):void{
			var tween:Tween;
			
			if(obj.blurX != newBlurX){
				tween = new Tween(obj, "blurX", Regular.easeInOut, obj.blurX, newBlurX, delay, true);
				tweenArray.push(tween);
			}
			if(obj.blurY != newBlurY){
				tween = new Tween(obj, "blurY", Regular.easeInOut, obj.blurY, newBlurY, delay, true);
				tweenArray.push(tween);
			}
			
			tween.addEventListener(TweenEvent.MOTION_CHANGE, function(){blurShape(obj);});
			tween.addEventListener(TweenEvent.MOTION_FINISH, removeBlurShapeEvent);

			if(isLastTween){
				tween.addEventListener(TweenEvent.MOTION_FINISH, lastTweenFinished);
			}
		}
		
		private function changePaint(obj:PlayerShapeObject, array:Array, isLastTween:Boolean = false):void{
			var tween:Tween = new Tween(obj, "changePaint", None.easeOut, 0, 100, delay, true);
			
			obj.paint(array);
			if(isLastTween){
				tween.addEventListener(TweenEvent.MOTION_FINISH, lastTweenFinished);
			}
			tweenArray.push(tween);
		}
		
		/**
		  Makes an object which is not a movie shape disappear and removes it when it
		  has done its job. This function is different from normal "disappear" because
		  it is less efficient, but not noticeable due to the uncommon use it has
		  @param func Function to call after the object has dissapeared. Null by default
		*/
		private function disappearNonShapeObject(obj:DisplayObject, func:Function = null, isLastTween:Boolean = false):void{
			var tween:Tween = new Tween(obj, "alpha", None.easeOut, 1, 0, delay, true);
			tween.addEventListener(TweenEvent.MOTION_FINISH, removeNonShapeObject);
			if(isLastTween){
				tween.addEventListener(TweenEvent.MOTION_FINISH, func);
			}
			tweenArray.push(tween);
		}
		
		/**
		  Non shape object is removed from its parent after disapearance.
		  This function is different from normal "removeObj" because
		  it is less efficient, but not noticeable due to the little use it has
		*/
		private function removeNonShapeObject(evt:TweenEvent):void{
			(evt.currentTarget as Tween).obj.removeEventListener(TweenEvent.MOTION_FINISH, removeNonShapeObject);
			(evt.currentTarget as Tween).obj.parent.removeChild((evt.currentTarget as Tween).obj);
		}
		
		/**
		  Object is removed from canvas after disapearance
		*/
		private function removeObj(evt:TweenEvent):void{
			(evt.currentTarget as Tween).obj.removeEventListener(TweenEvent.MOTION_FINISH, removeObj);
			canvas.removeChild((evt.currentTarget as Tween).obj as DisplayObject);
		}
		
		/**
		  Parses the specified action and marks it as the last one, if it is
		  @param action XML with the action to parse
		  @param lastOne Boolean indicating if its the last action (true) or not (false)
		*/
		private function parseAction(action:XML, lastOne:Boolean):void{
			var obj:PlayerShapeObject;
			switch(Action.translateToCode(action.type.text())){
				case Action.INSERT:
					obj = createPlayerShapeObject(action.id.text(), action.classtype.text());
					obj.x = Number(action.x);//+obj.width/2;
					obj.y = Number(action.y);//+obj.height/2;
					//if(action.text.toString().length > 0) obj.setText(action.text.text());
					if(action.child("text").length() > 0) obj.setText(action.text.text());
					if(action.rotate.toString().length > 0) obj.rotation = Number(action.rotate);
					if(action.scalex.toString().length > 0) obj.scaleX = Number(action.scalex);
					if(action.scaley.toString().length > 0) obj.scaleY = Number(action.scaley);
					if(action.blurx.toString().length > 0) obj.blurX = Number(action.blurx);
					if(action.blury.toString().length > 0) obj.blurY = Number(action.blury);
					if(action.paint.toString().length > 0) obj.paint(getPaintActionData(action.paint[0]));
					
					if(obj.blurX != 0 || obj.blurY != 0) obj.filters = [(new BlurFilter(obj.blurX, obj.blurY, BitmapFilterQuality.LOW))];
					
					appear(obj, lastOne);
					if(action.depth.toString().length > 0) setShapeDepth(obj, Number(action.depth));
					break;
				case Action.MOVE:
					obj = shapes.get(action.id.text());
					moveTo(obj, Number(action.num[0])/*+(obj.width/obj.scaleX)/2*/, Number(action.num[1])/*+(obj.height/obj.scaleY)/2*/, lastOne);
					break;
				case Action.CHANGE_TEXT:
					changeText(shapes.get(action.id.text()), action.text.text(), lastOne);
					break;
				case Action.CHANGE_DEPTH:
					changeDepth(shapes.get(action.id.text()), action.num, lastOne);
					break;
				case Action.REMOVE:
					disappear(shapes.get(action.id.text()), lastOne);
					break;
				case Action.SCALE:
					changeScale(shapes.get(action.id.text()), Number(action.num[0]), Number(action.num[1]), lastOne);
					break;
				case Action.ROTATE:
					changeRotation(shapes.get(action.id.text()), Number(action.num), lastOne);
					break;
				case Action.BLUR:
					changeBlur(shapes.get(action.id.text()), Number(action.num[0]), Number(action.num[1]), lastOne);
					break;
				case Action.PAINT:
					changePaint(shapes.get(action.id.text()), getPaintActionData(action.paint[0]), lastOne);
			}
		}
		
		/**
		  Blurs the shape everytime the tween changes the value of blurring
		*/
		private function blurShape(obj:PlayerShapeObject):void{
			//var obj:PlayerShapeObject = evt.currentTarget as PlayerShapeObject;
			obj.filters = [(new BlurFilter(obj.blurX, obj.blurY, BitmapFilterQuality.LOW))]
		}
		
		/**
		  Removes the blur shape "Motion Change" event from object
		*/
		private function removeBlurShapeEvent(evt:TweenEvent):void{
			evt.currentTarget.removeEventListener(TweenEvent.MOTION_CHANGE, function(){});
			evt.currentTarget.removeEventListener(TweenEvent.MOTION_FINISH, removeBlurShapeEvent);
		}
		
		/**
		  Transforms a string into a scene type code
		  @param str String representating the type of scene
		  @return uint with the code for sceneType
		*/
		private function getSceneTypeCode(str:String):uint{
			switch(str){
				case "instant":
					return SCENE_INSTANT;
				case "reader":
					return SCENE_READER;
				default:
					return SCENE_NORMAL;
			}
		}
		
		private function createPlayerShapeObject(id:String, classRef:String):PlayerShapeObject{
			var ClassReference:Class = getDefinitionByName("Player"+classRef) as Class;
			var dup:PlayerShapeObject = new ClassReference();
			dup.setFullID(id);
			shapes.insert(dup);
			canvas.addChild(dup);
			return dup;
		}
		
		private function clearCanvas():void{
			while(canvas.numChildren > 0){
				canvas.removeChildAt(canvas.numChildren-1);
			}
		}
		
		/**
		  Reads the XML action, assuming it is a paint action and
		  extracts all the information so a PlayerShapeObject is
		  able to read it
		  @param act XML action
		  @return Array
		*/
		private function getPaintActionData(act:XML):Array{
			var colorarray:Array = new Array(act.color.length()); // We will use push
			var linesarray:Array = new Array(act.line.length()); // We will use push
			var points:Array;
			var i:uint 		= 0;
			var j:uint		= 0;
			
			for(i = 0; i< act.color.length(); ++i){
				colorarray[i] = [uint(act.color[i]), uint(act.coloredlines[i])];
			}
			
			for(i = 0; i< act.line.length(); ++i){ //Number of lines
				points = new Array(act.line[i].x.length()); // Reserve number of points
				for(j = 0; j< act.line[i].x.length(); ++j){
					points[j] = [Number(act.line[i].x[j]), Number(act.line[i].y[j])];
				}
				linesarray[i] = points; // Number of points (each point is X and Y)
				
			}
			
			return [colorarray, linesarray];
		}
		
		/**
		  Sets the specified PlayerShapeObject to the indicated depth, leaving always the original
		  panel below
		  @param obj ShapeObject to change the depth, it is assumed this container has that PlayerShapeObject
		  @param depth uint with the new depth (absolute value)
		*/
		public function setShapeDepth(obj:PlayerShapeObject, depth:uint):void{
			// If title and description are present, there are two more children
			// in canvas than we expect, so the depth must be raised by two.
			// This would only happen in the first frame of the movie.
			if(tf_title.parent != null) ++depth;
			if(tf_description.parent != null) ++depth;
			
			// If the depth has a bug and is set higher than the number
			// of children, we fix it
			if(depth+1 >= canvas.numChildren) depth = canvas.numChildren-1-1;
			//var oldDepth:uint = canvas.getChildIndex(obj);
			canvas.setChildIndex(obj, depth+1);
			//var newDepth:uint = canvas.getChildIndex(obj);
			//trace("PlayerAnimation: setShapeDepth-> depth of "+obj.getFullID()+" change from "+String(oldDepth)+" to "+ String(newDepth));
		}
		
		
	}
}